﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Data;
using NGinnBPM.Lib.Interfaces;
using NGinnBPM.Lib.Schema;
using NGinnBPM.Lib.Data;
using NGinnBPM.Services;
using NGinnBPM.Runtime;
using NGinnBPM.Services.Utilities;
using System.IO;
using NLog;
using NGinnBPM.MessageBus;
using NGinnBPM.MessageBus.Impl;
using System.Timers;


namespace NGinnBPM.Dao
{
    public class RawSqlSerializedTaskInstanceRepository : ITaskInstanceRepository, IDisposable
    {
        private ITaskInstanceSerializer _serializer;
        private Logger log = LogManager.GetCurrentClassLogger();
        private Logger statlog = LogManager.GetLogger("STAT_TaskInstanceRepo");
        private TimedCache _tc = new TimedCache();
        private bool _inited = false;
        private Timer _tm = null;

        public RawSqlSerializedTaskInstanceRepository(ITaskInstanceSerializer serializer)
        {
            _serializer = serializer;
            _tc.Name = "TaskInstanceRepository";
            TableName = "TaskInstanceData";
            AutoCreateDatabase = true;
            ProcessStateRetentionPeriod = TimeSpan.Zero;
        }


        public ITaskDatabaseSessionFactory SessionFactory { get; set; }

        public string TableName { get; set; }
        public bool AutoCreateDatabase { get; set; }
        public TimeSpan ProcessStateRetentionPeriod { get; set; }

        private void Initialize()
        {
            if (_inited) return;
            lock (this)
            {
                if (_inited) return;
                _inited = true;
                try
                {
                    if (_tm != null)
                    {
                        _tm.Dispose();
                        _tm = null;
                    }
                    if (ProcessStateRetentionPeriod > TimeSpan.Zero)
                    {
                        log.Info("Starting periodic process state cleanup");
                        _tm = new Timer
                        {
                            AutoReset = true,
                            Interval = 60 * 1000
                        };
                        _tm.Elapsed += new ElapsedEventHandler(_tm_Elapsed);
                        _tm.Start();
                    }
                    log.Info("Task instance db table: {0}", TableName);
                    if (AutoCreateDatabase)
                    {
                        log.Info("Running db initialization script");
                        AccessDb(delegate(IDbConnection cn)
                        {
                            using (Stream stm = typeof(RawSqlSerializedTaskInstanceRepository).Assembly.GetManifestResourceStream("NGinnBPM.Dao.create_taskinstance_db.sql"))
                            {
                                StreamReader sr = new StreamReader(stm);
                                using (IDbCommand cmd = cn.CreateCommand())
                                {
                                    string txt = sr.ReadToEnd();
                                    cmd.CommandText = string.Format(txt, TableName);
                                    cmd.ExecuteNonQuery();
                                }
                            }
                        });
                    }
                }
                catch (Exception ex)
                {
                    log.Error("Error initializing task instance database: {0}", ex);
                    _inited = false;
                    throw;
                }
            }
        }

        void _tm_Elapsed(object sender, ElapsedEventArgs e)
        {
            try
            {
                CleanupOldProcessData();
            }
            catch (Exception ex)
            {
                log.Error("Error cleaning up the task db: {0}", ex);
            }
        }

        private void AccessDb(Action<IDbConnection> act)
        {
            Initialize();
            var ses = TaskDatabaseSession.CurrentSession;
            if (ses != null)
            {
                act((IDbConnection) ses.Connection);
            }
            else
            {
                using (ITaskDatabaseSession ses2 = SessionFactory.OpenSession())
                {
                    act((IDbConnection) ses2.Connection);
                }
            }
        }

        private void CleanupOldProcessData()
        {
            AccessDb(delegate(IDbConnection con)
            {
                DateTime dt = DateTime.Now;
                using (var cmd = con.CreateCommand())
                {
                    var sql = @"select top 10000 instance_id into #tmp_delete_tasks from {0} with(readpast) where task_type='ProcessInstance' and status = 3 and start_date <= @time_limit order by start_date
                                delete {0} with(readpast) where process_id in (select instance_id from #tmp_delete_tasks)
                                drop table #tmp_delete_tasks";
                    cmd.CommandText = string.Format(sql, TableName);
                    SqlUtil.AddParameter(cmd, "@time_limit", DateTime.Now - ProcessStateRetentionPeriod);
                    cmd.ExecuteNonQuery();
                }
                log.Debug("Deleting old processes took {0}", DateTime.Now - dt);
            });
        }
        #region ITaskInstanceRepository Members

        private string GetTaskState(string instanceId)
        {
            string st = (string)_tc.Get(instanceId);
            if (st != null) return st;
            AccessDb(delegate(IDbConnection con)
            {
                using (IDbCommand cmd = con.CreateCommand())
                {
                    cmd.CommandText = string.Format("select task_data from {0} with(nolock) where instance_id=@instance_id", TableName);
                    SqlDBUtil.AddParameter(cmd, "@instance_id", instanceId);
                    using (IDataReader dr = cmd.ExecuteReader())
                    {
                        if (!dr.Read()) return;
                        st = dr.GetString(0);
                        _tc.Put(instanceId, st);
                    }
                }
            });
            return st;
        }

        public NGinnBPM.Runtime.TaskInstance GetTaskInstance(string instanceId)
        {
            DateTime dt = DateTime.Now;
            string str = GetTaskState(instanceId);
            if (str == null) return null;
            TaskInstance ti = _serializer.DeserializeTaskInstance(new StringReader(str), null);
            statlog.Debug("GetTaskInstance|T:{0}", (DateTime.Now - dt).TotalMilliseconds);
            return ti;

        }

        public void UpdateTaskInstance(NGinnBPM.Runtime.TaskInstance inst)
        {
            DateTime dt = DateTime.Now;
            string sql = string.Format(@"update {0} with(rowlock) set 
               last_modified=getdate(), task_data=@task_data, version=@nversion, status=@status, scheduling_status=@scheduling_status, finish_date=@finish_date
                where instance_id=@instance_id and version=@version", TableName);

            int rversion = inst.PersistedVersion;
            inst.PersistedVersion = rversion + 1;
            inst.Passivate();
            StringWriter sw = new StringWriter();
            _serializer.SerializeTaskInstance(inst, sw);
            //log.Debug("Serialized task {0}: {1}", inst.InstanceId, sw.ToString());
            AccessDb(delegate(IDbConnection con)
            {
                using (IDbCommand cmd = con.CreateCommand())
                {
                    
                    cmd.CommandText = sql;
                    SqlDBUtil.AddParameter(cmd, "@instance_id", inst.InstanceId);
                    SqlDBUtil.AddParameter(cmd, "@status", (int)inst.Status);
                    SqlDBUtil.AddParameter(cmd, "@task_data", sw.ToString());
                    SqlDBUtil.AddParameter(cmd, "@finish_date", inst.FinishedDate);
                    SqlDBUtil.AddParameter(cmd, "@scheduling_status", inst.CanContinue ? (int)TaskSchedulingStatus.Ready : (int)TaskSchedulingStatus.Waiting);
                    SqlDBUtil.AddParameter(cmd, "@version", rversion);
                    SqlDBUtil.AddParameter(cmd, "@nversion", inst.PersistedVersion);
                    int rows = cmd.ExecuteNonQuery();
                    if (rows == 0)
                        throw new NGinnBPM.Lib.Exceptions.NGinnException("Record version mismatch when updating task instance " + inst.InstanceId).SetPermanent(false);
                }
            });
            _tc.Invalidate(inst.InstanceId);
            statlog.Debug("UpdateTaskInstance|T:{0}", (DateTime.Now - dt).TotalMilliseconds);
        }

        

        public void InsertTaskInstance(NGinnBPM.Runtime.TaskInstance inst)
        {
            DateTime dt = DateTime.Now;
            string sql = string.Format(@"INSERT INTO [{0}]
               ([instance_id]
               ,[definition_id]
               ,[task_id]
               ,[process_id]
               ,[parent_task_id]
               ,[version]
               ,[status]
               ,[start_date]
               ,[finish_date]
               ,[last_modified]
               ,[task_data]
               ,[task_type]
               ,[scheduling_status])
         VALUES
              (@instance_id, @definition_id, @task_id, @process_id, @parent_task_id, 0, @status, @start_date, @finish_date, getdate(), @task_data, @task_type, @scheduling_status)", TableName);

            inst.Passivate();
            StringWriter sw = new StringWriter();
            _serializer.SerializeTaskInstance(inst, sw);
            //log.Debug("Serialized task {0}: {1}", inst.InstanceId, sw.ToString());

            AccessDb(delegate(IDbConnection con)
            {
                using (IDbCommand cmd = con.CreateCommand())
                {
                    cmd.CommandText = sql;
                    SqlDBUtil.AddParameter(cmd, "@instance_id", inst.InstanceId);
                    SqlDBUtil.AddParameter(cmd, "@definition_id", inst.ProcessDefinitionId);
                    SqlDBUtil.AddParameter(cmd, "@task_id", inst.TaskId);
                    SqlDBUtil.AddParameter(cmd, "@process_id", inst.ProcessInstanceId);
                    SqlDBUtil.AddParameter(cmd, "@parent_task_id", inst.ParentTaskInstanceId);
                    SqlDBUtil.AddParameter(cmd, "@status", (int)inst.Status);
                    SqlDBUtil.AddParameter(cmd, "@start_date", DateTime.Now);
                    SqlDBUtil.AddParameter(cmd, "@task_data", sw.ToString());
                    SqlDBUtil.AddParameter(cmd, "@task_type", inst.GetType().Name);
                    SqlDBUtil.AddParameter(cmd, "@scheduling_status", inst.CanContinue ? (int)TaskSchedulingStatus.Ready : (int)TaskSchedulingStatus.Waiting);
                    SqlDBUtil.AddParameter(cmd, "@finish_date", inst.FinishedDate);
                    cmd.ExecuteNonQuery();
                }
            });
            statlog.Debug("InsertTask|T:{0}", (DateTime.Now - dt).TotalMilliseconds);
        }

        public IList<string> GetTaskInstancesReadyForProcessing(int maxNum)
        {
            List<string> lst = new List<string>();
            AccessDb(delegate(IDbConnection con)
            {
                string sql = string.Format("select instance_id from {3} with(readpast) where status in ({0}, {1}) and scheduling_status = {2}", (int)TaskStatus.Enabled, (int)TaskStatus.Selected, (int)TaskSchedulingStatus.Ready, TableName);
                using (IDbCommand cmd = con.CreateCommand())
                {
                    cmd.CommandText = sql;
                    using (IDataReader dr = cmd.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            lst.Add(dr.GetString(0));
                        }
                    }
                }
            });
            return lst;
        }

        
        public void UpdateTaskSchedulingStatus(string instanceId, NGinnBPM.Runtime.TaskSchedulingStatus status, string additionalInfo)
        {
            string sql = string.Format(@"update {0} with(rowlock) set 
               last_modified=getdate(), scheduling_status=@scheduling_status
                where instance_id=@instance_id", TableName);

            AccessDb(delegate(IDbConnection con)
            {
                using (IDbCommand cmd = con.CreateCommand())
                {
                    cmd.CommandText = sql;
                    SqlDBUtil.AddParameter(cmd, "@instance_id", instanceId);
                    SqlDBUtil.AddParameter(cmd, "@scheduling_status", (int)status);
                    cmd.ExecuteNonQuery();
                }
            });
        }

        /// <summary>
        /// TODO: implement
        /// </summary>
        /// <param name="processDefinition"></param>
        /// <param name="taskId"></param>
        public void SetWaitingTaskInstancesReady(string processDefinition, string taskId)
        {
            throw new NotImplementedException();
        }

        #endregion

        #region ITaskInstanceRepository Members

        /// <summary>
        /// TODO: implement
        /// </summary>
        /// <param name="processCorrelationId"></param>
        /// <param name="definitionId"></param>
        /// <returns></returns>
        public IList<string> FindProcessesByCorrelationId(string processCorrelationId, string definitionId)
        {
            throw new NotImplementedException();
        }

        #endregion


        #region stats

        private int ExecuteScalar(string sql)
        {
            int ret = 0;
            AccessDb(delegate(IDbConnection conn)
            {
                using (IDbCommand cmd = conn.CreateCommand())
                {
                    cmd.CommandText = sql;
                    ret = Convert.ToInt32(cmd.ExecuteScalar());
                }
            });
            return ret;
        }

        

       

       

        
        #endregion

        public void Dispose()
        {
            if (_tm != null)
            {
                _tm.Dispose();
                _tm = null;
            }
        }
    }
}
